package com.thtfpc.ssdp;

import com.thtfpc.utility.ThreadUtil;

import java.lang.ref.WeakReference;
import java.util.*;
import java.util.logging.Logger;

/**
 * 利用SSDP向组播网络中搜索目标设备或者服务
 * <pre>
 * 在SimpleServicFinder的构造函数中，需声明其找到的设备或者服务的String名称
 * SSDP设备或名称的规则协议包括：
 * ssdp:all 搜索所有设备和服务
 * upnp:rootdevice 仅搜索网络中的根设备
 * uuid:device-UUID 查询UUID标识的设备
 * urn:schemas-upnp-org:device:device-Type:version 查询device-Type字段指定的设备类型，设备类型和版本由UPNP组织定义。
 * urn:schemas-upnp-org:service:service-Type:version 查询service-Type字段指定的服务类型，服务类型和版本由UPNP组织定义。
 * </pre>
 * <pre>
 * {@code
 *  SimpleServiceFinder finder = new SimpleServiceFinder(
 *                      "urn:schemas-upnp-org:device:MyDevice:version",
 *                      new SimpleServiceFinder.SerachCallback() {
 *                          void onServiceOnlined(SSDPDevice){}
 *                          void onServiceOfflined(SSDPDevice){}},
 *                      port);
 *  finder.start();
 *  finder.stop();
 * }
 * </code>
 * </pre>
 * <pre>
 * 调用注意：
 * 1、finder会在调用start后，启动其查找线程，并在调用stop后停止查找线程。
 * 2、新设备或者服务被发现后，onServiceOnlined会被调用。
 * 3、同一设备在同一次search域内，不会被重复通知回调。
 * 4、同一SimpleServiceFinder 可反复start、stop操作，无需创建新实例。
 * </pre>
 */
public class SimpleServiceFinder extends SSDPChannel {

    static Logger logger = Logger.getLogger(SimpleServiceFinder.class.getSimpleName());

    public interface SearchCallback {
        void onServiceOnlined(SSDPDevice device);

        void onServiceOfflined(SSDPDevice device);
    }


    /**
     * 基于SSDP协议在网络中周期查找的线程类
     * <p>
     * SSDPSearchProcess 继承 Runnable，方便主类启动线程
     * <p>
     * SSDPSearchProcess的构造参数是外部类SimpleServiceFinder，利用WeakReferrence弱持有
     * 持有SimpleServiceFinder的主要目的是同步其isRunning方法，来判定当前启动的查找线程是否停止
     * <p>
     * run线程停止的信号是 SimpleServiceFinder.running == false || Thread.currentThread().interruped() == true
     * <p>
     * 同时在周期查找的间隙，调用sleep进行休眠，外部SimpleServiceFinder在stop后，可调用其interrupt来强行中断休眠
     */
    private static class SSDPSearchProcess implements Runnable {
        private WeakReference<SimpleServiceFinder> wrFinder;

        public SSDPSearchProcess(SimpleServiceFinder finder) {
            wrFinder = new WeakReference<>(finder);
        }

        @Override
        public void run() {
            do {
                SimpleServiceFinder simpleServiceFinder = wrFinder.get();
                if (simpleServiceFinder == null || simpleServiceFinder.isRunning() == false) {
                    break;
                }
                simpleServiceFinder.resetExpiredDevices();
                simpleServiceFinder.search();
                simpleServiceFinder = null;
                if (ThreadUtil.sleep(1000 * MAX_SECONDS_TO_WAIT)) {
                    break;
                }
            }
            while (Thread.currentThread().isInterrupted() == false);
            wrFinder.clear();
            logger.info("[SearchProcess]: search thread quit.");
        }
    }

    public static String TAG = SimpleServiceFinder.class.getSimpleName();
    protected static int MAX_SECONDS_TO_WAIT = 2;
    protected static int MAX_SECONDS_EXPIRED = MAX_SECONDS_TO_WAIT * 3;
    protected String target;
    protected Map<String, SSDPDevice> serviceFinded;
    protected WeakReference<SearchCallback> findCallbackWeakReference;
    protected Thread searchThread;

    /**
     * 默认监听1920端口
     *
     * @param target   查找的SSDP服务的全名
     * @param callback 查找事件回调对象
     */
    public SimpleServiceFinder(String target, SearchCallback callback) {
        this(target, callback, 1920);
    }


    /**
     * @param target   查找的SSDP服务的全名
     * @param callback 查找事件回调对象
     * @param port     服务监听的组播UDP端口号
     */
    public SimpleServiceFinder(String target, SearchCallback callback, int port) {
        super(port, true);
        this.target = target;
        this.serviceFinded = new LinkedHashMap<>();
        this.findCallbackWeakReference = new WeakReference<>(callback);
        this.searchThread = null;
    }


    @Override
    protected void onMessage(SSDPMessage ssdpMessage) {
        String searchTarget = null;
        String uniqueId = null;
        String serviceURL = null;

        /*
        被动模式下的查找
         */
        if ("HTTP/1.1 200 OK".equals(ssdpMessage.getMessageType())) {
            searchTarget = ssdpMessage.getHeader("ST");
            uniqueId = ssdpMessage.getHeader("USN");
            serviceURL = ssdpMessage.getHeader("LOCATION") != null ? ssdpMessage.getHeader("LOCATION") : ssdpMessage.getHeader("AL");
            onServiceFinded(
                    uniqueId,
                    searchTarget,
                    serviceURL,
                    ssdpMessage.getHeader("TITLE"),
                    ssdpMessage.getAddress(),
                    ssdpMessage.getPort());
            return;
        }

        /*
        主动模式下的查找，如果在主动模式下，注意，监听端口号只能是SSDP标准协议的端口1900
         */
        else if ("NOTIFY * HTTP/1.1".equals(ssdpMessage.getMessageType())
                && "ssdp:alive".equals(ssdpMessage.getHeader("NTS"))) {
            searchTarget = ssdpMessage.getHeader("NT");
            uniqueId = ssdpMessage.getHeader("USN");
            serviceURL = ssdpMessage.getHeader("LOCATION") != null ? ssdpMessage.getHeader("LOCATION") : ssdpMessage.getHeader("AL");
            onServiceFinded(
                    uniqueId,
                    searchTarget,
                    serviceURL,
                    ssdpMessage.getHeader("TITLE"),
                    ssdpMessage.getAddress(),
                    ssdpMessage.getPort());
            return;
        }

        /*
        有人宣布下线
         */
        else if ("NOTIFY * HTTP/1.1".equals(ssdpMessage.getMessageType())
                && "ssdp:byebye".equals(ssdpMessage.getHeader("NTS"))) {
            /*
               NOTIFY * HTTP/1.1
               Host: 239.255.255.250:reservedSSDPport
               NT: someunique:idscheme3
               NTS: ssdp:byebye
               USN: someunique:idscheme3
             */
            searchTarget = ssdpMessage.getHeader("NT");
            uniqueId = ssdpMessage.getHeader("USN");
            SSDPDevice deviceOfflined = null;
            if (searchTarget != null
                    && uniqueId != null
                    && target.equals(searchTarget)) {
                synchronized (serviceFinded) {
                    if (serviceFinded.containsKey(uniqueId)) {
                        deviceOfflined = serviceFinded.get(uniqueId);
                        serviceFinded.remove(uniqueId);
                    }
                }
                if (deviceOfflined != null) {
                    onServiceOfflined(deviceOfflined);
                }
            }
        }
    }

    protected void onServiceFinded(String deviceUniqueId,
                                   String searchTarget,
                                   String serviceURL,
                                   String deviceTitle,
                                   String srcAddr,
                                   int srcPort) {
        if (deviceUniqueId == null
                || searchTarget == null
                || serviceURL == null
                || target.equals(searchTarget) == false) {
            return;
        }
        synchronized (serviceFinded) {
            String uniqueServiceID = SSDPDevice.getServiceUniqueId(srcAddr, srcPort, serviceURL);
            if (serviceFinded.containsKey(uniqueServiceID) == false) {
                SSDPDevice device = new SSDPDevice();
                device.setId(deviceUniqueId);
                device.setName(searchTarget);
                device.setTitle(deviceTitle);
                device.setServiceUrl(serviceURL);
                device.setPort(srcPort);
                device.setAddress(srcAddr);
                device.setActiveAt(System.currentTimeMillis());
                serviceFinded.put(uniqueServiceID, device);
                onServiceOnlined(device);
            }
            else {
                SSDPDevice ssdpDevice = serviceFinded.get(uniqueServiceID);
                ssdpDevice.setActiveAt(System.currentTimeMillis());
            }
        }
    }


    /**
     * 向组播网络中发送查询信息
     * <p>
     * SSDP的查找协议格式如下：
     * M-SEARCH * HTTP/1.1
     * HOST: MulticastAddress:port
     * MAN: "ssdp:dicover"
     * MX: SECONDS_TO_WAIT
     * ST: SEARCH_TARGET
     */
    protected synchronized void search() {
        if (isRunning()) {
            SSDPMessage ssdpMessage = new SSDPMessageBuilder()
                    .messageType("M-SEARCH * HTTP/1.1")
                    .header("HOST", SSDPChannel.MULTICAST_ADDRESS + ":" + SSDPChannel.MULTICAST_PORT)
                    .header("MAN", "\"ssdp:discover\"")
                    .header("MX", String.valueOf(MAX_SECONDS_TO_WAIT))
                    .header("ST", target)
                    .address(SSDPChannel.MULTICAST_ADDRESS)
                    .port(SSDPChannel.MULTICAST_PORT)
                    .build();
            sendMessage(ssdpMessage);
        }
    }


    protected void onServiceOnlined(SSDPDevice deviceProviderService) {
        //System.out.println("onServiceOnlined:" + ssdpDevice);
        SearchCallback callback = findCallbackWeakReference.get();
        if (callback != null) {
            callback.onServiceOnlined(deviceProviderService);
        }
    }

    protected void onServiceOfflined(SSDPDevice deviceOfflined) {
        SearchCallback callback = findCallbackWeakReference.get();
        if (callback != null) {
            callback.onServiceOfflined(deviceOfflined);
        }
    }

    protected void resetExpiredDevices() {
        List<SSDPDevice> deviceExpired = new ArrayList<>();
        synchronized (serviceFinded) {
            long currentTimeStamp = System.currentTimeMillis();
            Iterator<Map.Entry<String, SSDPDevice>> iterator = serviceFinded.entrySet().iterator();
            while(iterator.hasNext()) {
                Map.Entry<String, SSDPDevice> entry = iterator.next();
                SSDPDevice ssdpDevice = entry.getValue();
                if((currentTimeStamp - ssdpDevice.getActiveAt()) >= MAX_SECONDS_EXPIRED * 1000) {
                    iterator.remove();
                    deviceExpired.add(ssdpDevice);
                }
            }
        }

        for (SSDPDevice ssdpDevice : deviceExpired) {
            onServiceOfflined(ssdpDevice);
        }
    }

    protected void onStart() {
        //for one thing, clear the last search result.
        synchronized (serviceFinded) {
            serviceFinded.clear();
        }
        //wait for last searching thread, until it's over.
        if (searchThread != null) {
            try {
                ThreadUtil.join(searchThread);
            }
            catch (Exception ex) {
                System.out.println(ex.toString());
            }
            searchThread = null;
        }
        searchThread = new Thread(new SSDPSearchProcess(this), "search_thread");
        searchThread.start();
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (searchThread != null) {
            //if thread is sleeping. interrupt it.
            searchThread.interrupt();
            //wait for thread over.
            try {
                ThreadUtil.join(searchThread);
            }
            catch (Exception ex) {
            }
            searchThread = null;
        }
    }
}
